// ****************************************************************************
// Microsoft Ocracoke
//
// Microsoft Confidential
// Copyright 2003 Microsoft Corporation. All Rights Reserved.
//
// ****************************************************************************
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.VisualStudio.TestTools.WebTesting;

namespace WebAndLoadTestPlugins
{
/// <summary>
/// ExtractText is an extraction rule for extracting text between two strings in the response.
/// By default, the first occurence is returned, but the Index property can be used to extract
/// additional occurances.
/// </summary>
public class ExtractText : ExtractionRule
{

// ****************************************************************************
// Public Methods
// ****************************************************************************
    

    /// <summary>
    /// Default constructor
    /// </summary>
    public ExtractText()
    {
    }

    /// <summary>
    /// Must be called after the rule properties have been set.
    /// </summary>
    internal void Initialize()
    {
        string startsWith = StartsWith;
        string endsWith = EndsWith;
        if (!UseRegularExpression)
        {
            startsWith = Regex.Escape(StartsWith);
            endsWith = Regex.Escape(EndsWith);
        }

        RegexOptions options = RegexOptions.Multiline;
        if (IgnoreCase)
        {
            options |= RegexOptions.IgnoreCase;
        }

        m_startsWithRegEx = new Regex(startsWith, options);
        m_endsWithRegEx = new Regex(endsWith, options);
    }

    /// <summary>
    /// string which delimits the start of the string to extract
    /// </summary>
    public string StartsWith
    {
        get { return m_startsWith; }
        set { m_startsWith = value; }
    }

    /// <summary>
    /// string which delimits the end of the string to extract
    /// </summary>
    public string EndsWith
    {
        get { return m_endsWith; }
        set { m_endsWith = value; }
    }

    /// <summary>
    /// true if matching is case-insensitive. Default is true.
    /// </summary>
    public bool IgnoreCase
    {
        get { return m_ignoreCase; }
        set { m_ignoreCase = value; }
    }

    /// <summary>
    /// true if the search strings are regular expressions. Default is false.
    /// </summary>

    public bool UseRegularExpression
    {
        get { return m_useRegularExpression; }
        set { m_useRegularExpression = value; }
    }

    /// <summary>
    /// If true, the request fails if the search text is not found.
    /// </summary>
    public bool Required
    {
        get { return m_required; }
        set { m_required = value; }
    }

    /// <summary>
    /// If true, the a random match is returned. If true, the Index property is ignored.
    /// </summary>
    public bool ExtractRandomMatch
    {
        get { return m_extractRandomMatch; }
        set { m_extractRandomMatch = value; }
    }

    /// <summary>
    /// 0-based Index of the string to match. By default, this rule returns the first occurance
    /// of StartsWith/EndsWith. Use Index to extract 
    /// </summary>
    public int Index
    {
        get { return m_index; }
        set { m_index = value; }
    }

// ****************************************************************************
// ExtractionRule Implementation
// ****************************************************************************
    public enum SearchResult
    {
        Found,
        StartsWithNotFound,
        EndsWithNotFound,
    }


    string m_document;


    /// <summary>
    /// Finds the next match.
    /// </summary>
    /// <returns>Found if start/end found, 
    /// else StartsWithNotFound, EndsWithNotFound.</returns>
    public SearchResult FindNextMatch(out int startIndex, out int endIndex)
    {
        Match startMatch;
        Match endMatch;
        startIndex = endIndex = 0;

        // Find the start
        startMatch = m_startsWithRegEx.Match(m_document, m_startSearch);
        if (!startMatch.Success)
            return SearchResult.StartsWithNotFound;

        // Find the end
        int startTextIndex = startMatch.Index + startMatch.Length;
        endMatch = m_endsWithRegEx.Match(m_document, startTextIndex);
        if (!endMatch.Success)
            return SearchResult.EndsWithNotFound;

        // Save the start and end index
        startIndex = startTextIndex;
        endIndex = endMatch.Index;
        return SearchResult.Found;
    }

    /// <summary>
    /// Does the work to find the text.
    /// </summary>
    /// <param name="document">string to search</param>
    /// <returns>Found if start/end found, 
    /// else StartsWithNotFound, EndsWithNotFound.</returns>
    public SearchResult Extract()
    {
        int startIndex;
        int endIndex;
        int index = 0;

        while (m_startSearch < m_document.Length)
        {
            // Find the next match
            SearchResult result = FindNextMatch(out startIndex, out endIndex);
            if (result != SearchResult.Found)
            {
                return result;
            }

            // Return this one if it is the right index
            if (index == Index)
            {
                m_result = m_document.Substring(startIndex, endIndex - startIndex);
                return SearchResult.Found;
            }

            // Move to the next index
            m_startSearch = startIndex;
            index++;
        }
        return SearchResult.StartsWithNotFound;
    }

    public SearchResult ExtractRandom()
    {
        int startIndex;
        int endIndex;
        SearchResult result;
        List<int> startIndexes = new List<int>();
        List<int> endIndexes = new List<int>();

        // Loop until we've found all matches
        while (m_startSearch < m_document.Length)
        {
            result = FindNextMatch(out startIndex, out endIndex);
            if (result != SearchResult.Found)
            {
                break;
            }

            // Save the start and end index
            startIndexes.Add(startIndex);
            endIndexes.Add(endIndex);

            m_startSearch = startIndex;
        }

        if (startIndexes.Count > 0)
        {
            // Randomly select one of the indexes
            int random = new Random().Next(startIndexes.Count);
            m_result = m_document.Substring(startIndexes[random], endIndexes[random] - startIndexes[random]);
            return SearchResult.Found;
        }
        else
        {
            return SearchResult.StartsWithNotFound;
        }
    }

    /// <summary>
    /// Specify a localizable user readable name for use in the user interface.
    /// </summary>
    public override string RuleName
    {
        get { return "Custom Extract Text"; }
    }

    /// <summary>
    /// Specify a localizable user readable description for use in the user interface.
    /// </summary>
    public override string RuleDescription
    {
        get { return "Same functionality as Extract Text rule"; }
    }

    /// <summary>
    /// This override is provided so it can be called from a unit test. 
    /// Searches for the StartsWith string, then finds the first EndsWith string.
    /// If Index > 0, search starts at the ends of the previous StartsWith string.
    /// </summary>
    /// <param name="document">response</param>
    public SearchResult Extract(string document)
    {
        Initialize();
        m_document = document;
        SearchResult result;
        if (ExtractRandomMatch)
        {
            result = ExtractRandom();
        }
        else
        {
            result = Extract();
        }

        return result;
    }

    /// <summary>
    /// Searches for the StartsWith string, then finds the first EndsWith string.
    /// If Index > 0, search starts at the ends of the previous StartsWith string.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e">contains request, response, context</param>
    public override void Extract(object sender, ExtractionEventArgs e)
    {
        SearchResult result;
        result = Extract(e.Response.BodyString);

        e.Success = !m_required || result == SearchResult.Found;

        if (result == SearchResult.Found)
        {
            e.WebTest.Context[this.ContextParameterName] = m_result;
        }
        else
        {
            if (result == SearchResult.StartsWithNotFound)
            {
                e.Message = "StartsWith not found";
            }
            else
            {
                e.Message = "EndWith not found";
            }
        }
    }

    // ************************************************************************
    // ************************************************************************

    public string Result
    {
        get { return m_result; }
    }

    // ****************************************************************************
    // Private Members
    // ****************************************************************************

    // Properties for saving the tag and attribute information
    private string m_startsWith;
    private string m_endsWith;
    private Regex m_startsWithRegEx;
    private Regex m_endsWithRegEx;
    private int m_startSearch;
    
    // If the header is required and not found Extract() returns false
    // (Required defaults to true)
    private bool m_required = true;
    private bool m_extractRandomMatch = false;
    private int m_index;
    private bool m_useRegularExpression = false;
    private bool m_ignoreCase = false;
    private string m_result;

}   
}
